Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| throw AndroidSnaptestingNoDeviceProviderInstrumentTestTasksException() | ||
| } | ||
|
|
||
| val extension = project.extensions.findByType(TestedExtension::class.java) |
There was a problem hiding this comment.
AGP 9.x removed TestedExtension from the public API. We now use the stable
variant API (AndroidComponentsExtension / ApplicationAndroidComponentsExtension) which is the recommended way to interact with Android build variants since AGP 7.x.
| .firstOrNull { it.name == deviceProviderTask.variantName } | ||
| ?: throw RuntimeException("TestVariant not found for ${deviceProviderTask.variantName}") | ||
| val applicationIdProvider = providerFactory.provider { testedVariant.applicationId } | ||
| val adbExecutablePath = extension.adbExecutable.absolutePath |
There was a problem hiding this comment.
adbExecutable was removed from TestedExtension in AGP 9.x.
sdkComponents.adb is the stable replacement: it returns a lazy Provider pointing to the adb binary resolved from the configured SDK.
| it.iDevice.executeShellCommand("rm -rf ${getDeviceAndroidSnaptestingRootAbsolutePath()}", receiver) | ||
| println(receiver.output) | ||
| devices.forEach { device -> | ||
| runAdb(device.serialNumber, "shell", "rm", "-rf", getDeviceAndroidSnaptestingRootAbsolutePath()) |
There was a problem hiding this comment.
Previously used DDMLib's IDevice.executeShellCommand() to run 'rm -rf' on the device. Now delegates to runAdb(), which spawns an 'adb -s shell rm -rf ...' subprocess.
|
|
||
| private fun getDeviceAndroidSnaptestingRootAbsolutePath(): String = | ||
| "${FileListingService.DIRECTORY_SDCARD}/Download/android-snaptesting/$applicationId" | ||
| "/sdcard/Download/android-snaptesting/$applicationId" |
There was a problem hiding this comment.
FileListingService.DIRECTORY_SDCARD was a DDMLib constant ("/sdcard"). Using the literal string directly removes the DDMLib dependency entirely.
| destinationPath: String, | ||
| ) { | ||
| val fileEntry = getDeviceAndroidSnaptestingSubfolderAbsolutePath(androidSnaptestingSubFolderInDevice).toFileEntry() | ||
| val remotePath = getDeviceAndroidSnaptestingSubfolderAbsolutePath(androidSnaptestingSubFolderInDevice) |
There was a problem hiding this comment.
The old implementation built a FileEntry tree using DDMLib's FileListingService and then called IDevice.pullFile() for each entry.
The new implementation runs adb shell ls <remotePath> to list files and
adb pull <remote> <local> for each one, filtering out error lines from ls in case the folder doesn't exist yet on the device.
| println(output) | ||
| } | ||
|
|
||
| private fun runAdbCapture(serial: String, vararg args: String): String { |
There was a problem hiding this comment.
runAdbCapture spawns a subprocess with the adb binary, targeting a specific device by serial (-s flag), merges stderr into stdout, and waits up to 60 seconds.
There was a problem hiding this comment.
I would probably consider adding error handling for ADB command failures (e.g., check exit codes, handle timeouts, log errors) to improve robustness.”
Perhaps something like:
private fun runAdbCapture(
serial: String,
vararg args: String,
throwOnError: Boolean = false
): AdbResult {
val command = buildList {
add(adbExecutablePath)
add("-s")
add(serial)
addAll(args.toList())
}
try {
val process = ProcessBuilder(command)
.redirectErrorStream(false)
.start()
val output = process.inputStream.bufferedReader().readText()
val error = process.errorStream.bufferedReader().readText()
val finished = process.waitFor(60, TimeUnit.SECONDS)
val exitCode = process.exitValue()
if (!finished || exitCode != 0) {
val message = "ADB command failed: ${command.joinToString(" ")}\nExit code: $exitCode\nOutput: $output\nError: $error"
if (throwOnError) throw RuntimeException(message)
else println(message)
}
return AdbResult(output, error, exitCode)
} catch (e: Exception) {
val message = "Exception running ADB command: ${command.joinToString(" ")}\n${e.message}"
if (throwOnError) throw RuntimeException(message, e)
else println(message)
return AdbResult("", e.message ?: "", -1)
}
}
private data class AdbResult(val output: String, val error: String, val exitCode: Int)And then using it as: val result = runAdbCapture(serial, *args, throwOnError = true) in runAdb
| ) { | ||
| device.fileListingService.getChildrenSync(androidSnaptestingDeviceFolder).forEach { | ||
| device.pullFile(it.fullPath, "$destinationPath/${it.name}") | ||
| private fun runAdb(serial: String, vararg args: String) { |
There was a problem hiding this comment.
Convenience wrapper that additionally prints the output to the Gradle log. Both replace the DDMLib IDevice shell/pull APIs removed in AGP 9.x.
|
|
||
| project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java) | ||
| ?.onVariants { variant -> | ||
| // variant.name == "debug" → test task variant name == "debugAndroidTest" |
There was a problem hiding this comment.
is this comment in the code necessary?
| println(output) | ||
| } | ||
|
|
||
| private fun runAdbCapture(serial: String, vararg args: String): String { |
There was a problem hiding this comment.
I would probably consider adding error handling for ADB command failures (e.g., check exit codes, handle timeouts, log errors) to improve robustness.”
Perhaps something like:
private fun runAdbCapture(
serial: String,
vararg args: String,
throwOnError: Boolean = false
): AdbResult {
val command = buildList {
add(adbExecutablePath)
add("-s")
add(serial)
addAll(args.toList())
}
try {
val process = ProcessBuilder(command)
.redirectErrorStream(false)
.start()
val output = process.inputStream.bufferedReader().readText()
val error = process.errorStream.bufferedReader().readText()
val finished = process.waitFor(60, TimeUnit.SECONDS)
val exitCode = process.exitValue()
if (!finished || exitCode != 0) {
val message = "ADB command failed: ${command.joinToString(" ")}\nExit code: $exitCode\nOutput: $output\nError: $error"
if (throwOnError) throw RuntimeException(message)
else println(message)
}
return AdbResult(output, error, exitCode)
} catch (e: Exception) {
val message = "Exception running ADB command: ${command.joinToString(" ")}\n${e.message}"
if (throwOnError) throw RuntimeException(message, e)
else println(message)
return AdbResult("", e.message ?: "", -1)
}
}
private data class AdbResult(val output: String, val error: String, val exitCode: Int)And then using it as: val result = runAdbCapture(serial, *args, throwOnError = true) in runAdb
| # resources declared in the library itself and none from the library's dependencies, | ||
| # thereby reducing the size of the R class for that library | ||
| android.nonTransitiveRClass=true No newline at end of file | ||
| # Non-transitive R classes are the default in AGP 9.x and above |
There was a problem hiding this comment.
Is this comment necessary?
| // Collect applicationId per test-variant name at configuration time using the new variant API. | ||
| // onVariants runs during project configuration, before afterEvaluate. |
There was a problem hiding this comment.
In my opinion, this comment is more of a PR comment than a comment that should be in the code.
🎟️ Jira ticket
ANDROID-17464
🥅 What's the goal?
Migrate the Gradle plugin from deprecated AGP internal APIs to the modern stable APIs, ensuring compatibility with newer AGP versions.
Before merging this PR, I will create an RC1 and try this new version in other projects
🚧 How do we do it?
TestedExtensionwithAndroidComponentsExtension+ApplicationAndroidComponentsExtension(new variant API) to resolveapplicationIdandadbpath.applicationIdper variant at configuration time usingonVariants {}as a lazyProvider<String>.ddmlibusage (IDevice,FileListingService,CollectingOutputReceiver) inDeviceFileManagerand replace with directadbsubprocess calls viaProcessBuilder.📘 Documentation changes?
🧪 How can I test this?
Run the connected Android tests with and without record mode to verify snapshots are pulled and reports are generated correctly.
Grabacion.de.pantalla.2026-03-30.a.las.16.21.22.mov
Grabacion.de.pantalla.2026-03-30.a.las.16.23.33.mov